function ExtendArray(): void {
    Array.prototype.first = function <T>(predicate?: (item: T) => boolean): T | undefined {
        let array = (<Array<T>>this);

        if (predicate == null) {
            return array.at(0);
        }

        for (let i = 0; i < (<Array<T>>this).length; i++) {
            let item = array[i];
            if (predicate(item)) {
                return item;
            }
        }

        return undefined;
    }

    Array.prototype.firstOrDefault = function <T>(predicate: (item: T) => boolean, value: T): T | null {
        let array = (<Array<T>>this);
        for (let i = 0; i < (<Array<T>>this).length; i++) {
            let item = array[i];
            if (predicate(item)) {
                return item;
            }
        }
        return value;
    }

    Array.prototype.where = function <T>(predicate: (item: T) => boolean): Array<T> {
        let result = Array<T>();
        for (let i = 0; i < (<Array<T>>this).length; i++) {
            let item = (<Array<T>>this)[i];
            if (predicate(item)) {
                result.push(item);
            }
        }
        return result;
    }

    Array.prototype.remove = function <T>(item: T): boolean {
        let index = (<Array<T>>this).indexOf(item);
        if (index >= 0) {
            (<Array<T>>this).splice(index, 1);
            return true;
        }
        return false;
    }

    Array.prototype.removeAt = function <T>(index: number, length: number = 1): boolean {
        if (index >= 0) {
            (<Array<T>>this).splice(index, length);
            return true;
        }
        return false;
    }

    Array.prototype.removeRange = function <T>(items: T[]): void {
        for (let i = 0; i < items.length; i++) {
            (<Array<T>>this).remove(items[i]);
        }
    }

    Array.prototype.add = function <T>(item: T): void {
        (<Array<T>>this).push(item);
    }

    Array.prototype.addRange = function <T>(items: T[]): void {
        for (let i = 0; i < items.length; i++) {
            (<Array<T>>this).push(items[i]);
        }
    }

    Array.prototype.groupBy = function <T, K extends keyof any>(propertyExpression: (item: T) => K): Map<K, T[]> {
        return (<Array<T>>this).reduce((previous, current) => {
            const group = propertyExpression(current);
            if (!previous.has(group)) {
                previous.set(group, new Array<T>());
            }
            previous.get(group)!.push(current);
            return previous;
        }, new Map<K, T[]>());
    }

    Array.prototype.orderBy = function <T, K>(propertyExpression: (item: T) => K): Array<T> {
        let result = Array<T>();
        const compareFunction = (item1: T, item2: T): number => {
            if (propertyExpression(item1) > propertyExpression(item2)) return 1;
            if (propertyExpression(item2) > propertyExpression(item1)) return -1;
            return 0;
        };
        for (let i = 0; i < (<Array<T>>this).length; i++) {
            return (<Array<T>>this).sort(compareFunction);
        }
        return result;
    }

    Array.prototype.orderByDescending = function <T, K>(propertyExpression: (item: T) => K): Array<T> {
        let result = Array<T>();
        const compareFunction = (item1: T, item2: T): number => {
            if (propertyExpression(item1) > propertyExpression(item2)) return -1;
            if (propertyExpression(item2) > propertyExpression(item1)) return 1;
            return 0;
        };
        for (let i = 0; i < (<Array<T>>this).length; i++) {
            return (<Array<T>>this).sort(compareFunction);
        }
        return result;
    }

    Array.prototype.orderByMany = function <T, K>(propertyExpressions: [(item: T) => K]): Array<T> {
        let result = Array<T>();
        const compareFunction = (item1: T, item2: T): number => {
            for (let i = 0; i < propertyExpressions.length; i++) {
                let propertyExpression = propertyExpressions[i];
                if (propertyExpression(item1) > propertyExpression(item2)) return 1;
                if (propertyExpression(item2) > propertyExpression(item1)) return -1;
            }
            return 0;
        };
        for (let i = 0; i < (<Array<T>>this).length; i++) {
            return (<Array<T>>this).sort(compareFunction);
        }
        return result;
    }

    Array.prototype.orderByManyDescending = function <T, K>(propertyExpressions: [(item: T) => K]): Array<T> {
        let result = Array<T>();
        const compareFunction = (item1: T, item2: T): number => {
            for (let i = 0; i < propertyExpressions.length; i++) {
                let propertyExpression = propertyExpressions[i];
                if (propertyExpression(item1) > propertyExpression(item2)) return -1;
                if (propertyExpression(item2) > propertyExpression(item1)) return 1;
            }
            return 0;
        };
        for (let i = 0; i < (<Array<T>>this).length; i++) {
            return (<Array<T>>this).sort(compareFunction);
        }
        return result;
    }

    Array.prototype.clear = function <T>(): void {
        (<Array<T>>this).splice(0, (<Array<T>>this).length);
    }

    Array.prototype.shuffle = function <T>(): Array<T> {
        let array = (<Array<T>>this);

        for (let j, x, i = array.length; i; j = Math.floor(Math.random() * i), x = array[--i], array[i] = array[j], array[j] = x);

        return array;
    }

    Array.empty = function <T>(): Array<T> {
        return new Array<T>();
    }
}

function ExtendMap() {
    Map.prototype.select = function <K, V, T>(selector: (key: K, value: V) => T): Array<T> {
        let result = Array<T>();
        for (const [key, value] of (<Map<K, V>>this)) {
            result.add(selector(key, value));
        }
        return result;
    }
}

function ExtendDate(): void {
    Date.prototype.format = function (value: string): string {
        let date = this as Date;
        const options = {
            'M+': date.getMonth() + 1,
            'd+': date.getDate(),
            'h+': date.getHours(),
            'm+': date.getMinutes(),
            's+': date.getSeconds(),
            'q+': Math.floor((date.getMonth() + 3) / 3),
            'f+': date.getMilliseconds(),
        };

        const year = new RegExp(`(y+)`).exec(value);
        if (year != null) {
            value = value.replace(year[0], (this.getFullYear() + '').substring(4 - year[0].length));
        }

        for (const [key, data] of Object.entries(options)) {
            const result = new RegExp(`(${key})`).exec(value);
            if (result != null) {
                value = value.replace(result[0], (result[0].length == 1) ? `${data}` : `${data}`.substring(0, result[0].length).padStart(result[0].length, '0'));
            }
        }

        return value;
    }
}

function ExtendMath(): void {
    Math.clamp = function (value: number, min: number, max: number): number {
        return Math.min(Math.max(value, min), max);
    }
}

function ExtendObject(): void {
    Object.IsOfType = function <T>(target: unknown, property: keyof T): target is T {
        return (target as T)[property] !== undefined;
    }
}

/**
 * JavaScriptUtility
 */
export default class JavaScriptUtility {
    private static instance: JavaScriptUtility = new JavaScriptUtility();
    /**
     * JavaScriptUtility
     */
    public static get Default(): JavaScriptUtility {
        return this.instance;
    }

    constructor() {
        window.addEventListener('unload', this.Dispose.bind(this));
    }

    Dispose() {
        window.removeEventListener('unload', this.Dispose);
    }
    /**
     * 初始化JavaScript扩展
     */
    public Initialize(): void {
        ExtendArray();
        ExtendMap();
        ExtendDate();
        ExtendMath();
        ExtendObject();
    }
}